这是JS 原生方法原理探究系列的第七篇文章。本文会介绍如何实现 Object.assign() 方法。

Object.assign() 的基本用法

要实现 Object.assign(),首先了解它的大概用法:

  • 接受的第一个参数表示目标对象(浅拷贝的结果),如果是 null 或者 undefined,直接报错;如果是对象字面量或者数组,直接使用;如果是基本类型,则装箱为对应的对象。
  • 如果只接受了第一个参数,则将其包装为对象直接返回;如果不止接受了第一个参数,比如说接受了第二,第三 …… 等多个参数,那么这些参数表示源对象,它们的自身可枚举属性会一一添加到目标对象上,属性同名则以靠后的对象为准,进行属性覆盖。
  • 第一个参数往后的参数,如果是 null 或者 undefined,那么直接跳过;其余的情况则尝试找出它们的可枚举属性,但实际上,只有字符串、数组、对象字面量这些类型是具有可枚举属性的。

实现代码

根据上面所讲的思路,实现的代码就是下面这样的:

function myAssign(target,...objs){
    if(target === null || target === undefined){
        throw new TypeError("can not convert null or undefined to object")
    }
    let res = Object(target)
    objs.forEach(obj => {
        'use strict'
        if(obj != null && obj != undefined){
            for(let key in obj){
                if(Object.prototype.hasOwnProperty.call(obj,key)){
                    res[key] = obj[key]
                }
            }
        }
    })
    return res
}
Object.defineProperty(Object,'myAssign',{
    value: myAssign,
    writable: true,
    configurable: true,
    enumerable: false
})

需要注意的要点

需要注意的要点如下:

为什么不直接通过 . 给 Object 添加 myAssign 方法?

Object.myAssign() 实际上是 Object 的一个静态方法,但是不要直接通过 . 添加,因为这种方式添加的方法是可以枚举的,而 assign() 方法不可枚举。所以这里使用 Object.defineProperty() 添加,同时设置该方法不可枚举、可读、可配置。

为什么要使用严格模式?

考察参数出现字符串的情况。下面这两种情况容易理解:

Object.assign({a:1},"cd")     
// 把 "cd" 的可枚举属性 0 和 1 添加到目标对象上,最后得到 {a:1,0:“c”,1:"d"}

Object.assign("cd",{a:1})
// 把 {a:1} 的可枚举属性 a 添加到目标对象上,最后得到 String{“cd”,a:1}

但如果是这种情况:

Object.assign("ab","cd")    
// 报错 Cannot assign to read only property '0' of object '[object String]'

这里尝试把 “cd” 的可枚举属性 0 和 1 添加到目标对象上,但问题是,目标对象 String{“ab”} 也有可枚举属性 0 和 1,而且是只读的,这意味着我们尝试去修改目标对象的只读属性,所以报错也就很合理了。但是,在非严格模式下,这种行为只会静默失败,为了让它真的抛出错误,必须声明使用严格模式。

为什么不使用 Reflect.ownKeys(obj)

考虑目标对象和源对象都是数组的情况,使用 Reflect.ownKeys(obj)确实可以一次性获得 obj 的自身可枚举属性,但是这些属性除了数组索引之外,也包含数组长度,这会导致将源对象的数组长度作为目标对象的数组长度,但实际上,两者长度不一定相等。比如,Objetc.myAssign([1,2,3],[8,9]) 的结果将不是期望得到的 [8,9,3],而是 [8,9],因为目标对象的长度被覆盖了。

为什么不直接使用 obj.hasOwnProperty(key)

既然不能使用 Reflect.ownKeys(obj),那么就只有先使用 for(let key in obj) 获得 obj 的自身属性 + 原型链属性,再使用 obj.hasOwnProperty(key) 筛选出自身属性了。但是为什么不直接使用 obj.hasOwnProperty(key) 呢?

这是因为,我们对源对象的情况并不了解。一方面,它可能重写了 hasOwnProperty 方法;另一方面,它可能是基于 Object.create(null) 构建的,这样的对象不会从 Object 原型上继承 hasOwnProperty 方法。所以这里借用 Object 原型的 hasOwnProperty 方法,是最保险的方式。